package hu.bme.mit.androtext.generator;

import hu.bme.mit.androtext.age.age.AndroidApplication;
import hu.bme.mit.androtext.generator.activity.GenerateActivityClasses;
import hu.bme.mit.androtext.generator.app.AndroidConstants2;
import hu.bme.mit.androtext.generator.app.GenerateAndroidManifest;
import hu.bme.mit.androtext.generator.app.GenerateStringValues;
import hu.bme.mit.androtext.generator.entity.GenerateEntityClasses;
import hu.bme.mit.androtext.generator.entity.GenerateEntityTables;
import hu.bme.mit.androtext.generator.util.ExtensionUtilDelegate;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.xpand2.output.JavaBeautifier;
import org.eclipse.xpand2.output.NoChangesVetoStrategy;
import org.eclipse.xpand2.output.Outlet;
import org.eclipse.xpand2.output.Output;
import org.eclipse.xpand2.output.OutputImpl;
import org.eclipse.xpand2.output.PostProcessor;
import org.eclipse.xtend.typesystem.xsd.XMLBeautifier;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
import com.android.ide.eclipse.adt.internal.project.AndroidNature;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.io.StreamException;

@SuppressWarnings("restriction")
public class AppGenerator implements IAppGenerator {

	private List<IGeneratorFragment> fragments = new ArrayList<IGeneratorFragment>();
	private Map<String, IFolder> packageNamesToPackageFolders;

	private Map<URI, Resource> loadedResources = new HashMap<URI, Resource>();
	private ResourceSet mainResourceSet = new ResourceSetImpl();

	private final IFile source;
	private AndroidApplication application;
	private IProject project;
	private IProjectDescription projectDescription;
	private List<String> allPackageNames;
	private Output output;
	private IProject modelProject;
	private JavaBeautifier javaBeautifier = new JavaBeautifier();
	private XMLBeautifier xmlBeautifier = new XMLBeautifier();

	public AppGenerator(IFile source) throws AppGenerationException {
		this.source = source;
		modelProject = getSource().getProject();
		initializeFragments();
		EObject root = getResourceRoot(getSource());
		if (root instanceof AndroidApplication) {
			application = (AndroidApplication) root;
		} else {
			throw new AppGenerationException("The root of the model is not AndroidApplication object!");
		}
		// FIXME: dirty hack class field
		ExtensionUtilDelegate.setGenerator(this);
	}

	private void initializeFragments() {
		addGeneratorFragment(new GenerateEntityClasses(this));
		addGeneratorFragment(new GenerateEntityTables(this));
		addGeneratorFragment(new GenerateActivityClasses(this));
		addGeneratorFragment(new GenerateStringValues(this));
		addGeneratorFragment(new GenerateAndroidManifest(this));
	}

	/**
	 * Adds a generator fragment to the main generator.
	 * 
	 * @param fragment
	 */
	protected void addGeneratorFragment(AbstractGeneratorFragment fragment) {
		fragments.add(fragment);
	}

	/**
	 * Loads a resource if not loaded already and returns the root object.
	 * 
	 * @param source
	 * @return
	 * @throws AppGenerationException
	 */
	public EObject getResourceRoot(IFile source)
			throws AppGenerationException {
		try {
			URI uri = URI.createFileURI(source.getFullPath().toOSString());
			Resource res = loadedResources.get(uri);
			if (res == null) {
				Resource model = mainResourceSet.getResource(uri, true);
				if (model != null) {
					loadedResources.put(uri, model);
					System.out.println("Loaded resource: " + uri);
					return model.getContents().get(0);
				} else {
					throw new AppGenerationException(
							"The model is wrong! No contents, or not a correct model file!");
				}
			} else {
				return res.getContents().get(0);
			}
		} catch (Exception e) {
			throw new AppGenerationException(e.getMessage());
		}
	}

	@Override
	public void generate(IProgressMonitor monitor)
			throws AppGenerationException, CoreException {
		// generate the project if not exists
		generateProject(monitor);
		if (getProject() == null)
			throw new AppGenerationException("The project is null!");
		if (monitor.isCanceled())
			throw new OperationCanceledException();
		monitor.subTask("Generating Packages and Folders");
		// generate the packages
		IFolder src = getProject().getFolder(FN_SRC_GEN);
		packageNamesToPackageFolders = createPackages(src,
				getAllPackageNames(), new SubProgressMonitor(monitor, 10));
		initializeOutput();
		// all setup complete do the generation
		doGenerate(monitor);
		if (monitor.isCanceled())
			throw new OperationCanceledException();
		monitor.subTask("Refreshing the project");
		// refresh the project folder
		getProject().refreshLocal(IResource.DEPTH_INFINITE,
				new SubProgressMonitor(monitor, 10));
	}
	/**
	 * Actual code and xml generation
	 * @throws AppGenerationException 
	 */
	private void doGenerate(IProgressMonitor monitor) throws AppGenerationException {
		// invoke the generator framework
		for (IGeneratorFragment gf : fragments) {
			if (monitor.isCanceled())
				throw new OperationCanceledException();
			gf.generate(monitor, application);
		}
	}

	/**
	 * Creates a default output for the create projects SRC folder.
	 * 
	 * @return
	 */
	private Output initializeOutput() {
		output = new OutputImpl();
		for (Outlet o : createOutlets()) {
			output.addOutlet(o);
		}
		return output;
	}

	/**
	 * Creates Outlets from the packages.
	 * 
	 * @return
	 */
	private List<Outlet> createOutlets() {
		List<Outlet> outlets = new ArrayList<Outlet>();
		for (String packageName : packageNamesToPackageFolders.keySet()) {
			IFolder folder = packageNamesToPackageFolders.get(packageName);
			outlets.add(createOutletForPackage(packageName, folder));
		}
		outlets.add(createOutletForFolder("PROJECT", getProject()));
		outlets.add(createOutletForFolder(null, getProject().getFolder(FN_SRC_GEN)));
		outlets.add(createOutletForFolder(null, getProject().getFolder(AndroidConstants2.RES_DIRECTORY + SdkConstants.FD_DRAWABLE + AndroidConstants.WS_SEP)));
		outlets.add(createOutletForFolder(null, getProject().getFolder(AndroidConstants2.RES_DIRECTORY + AndroidConstants2.LAYOUT_DIRECTORY)));
		outlets.add(createOutletForFolder(null, getProject().getFolder(AndroidConstants2.RES_DIRECTORY + AndroidConstants2.VALUES_DIRECTORY)));
		outlets.add(createOutletForFolder(null, getProject().getFolder(AndroidConstants2.RES_DIRECTORY + "xml/")));
		return outlets;
	}

	private Outlet createOutletForPackage(String packageName, IFolder folder) {
		String location = folder.getLocation().toOSString();
		List<PostProcessor> postProcessors = new ArrayList<PostProcessor>();
		postProcessors.add(javaBeautifier);
		return createOutlet(getNameForPackageName(packageName), location, postProcessors);
	}

	private Outlet createOutletForFolder(String name, IContainer folder) {
		String location = folder.getLocation().toOSString();
		if (name == null) {
			name = folder.getName();
		}
		List<PostProcessor> postProcessors = new ArrayList<PostProcessor>();
		postProcessors.add(javaBeautifier);
		postProcessors.add(xmlBeautifier);
		return createOutlet(name, location, postProcessors);
	}

	private Outlet createOutlet(String name, String outletPath, List<PostProcessor> postProcessors) {
		Outlet o = new Outlet(outletPath);
		o.setName(name);
		for (PostProcessor postProcessor : postProcessors) {
			o.addPostprocessor(postProcessor);
		}
		o.addVetoStrategy(new NoChangesVetoStrategy());
		o.setFileEncoding(FILE_ENCODING);
		System.out.println("Outlet created with name: " + o.getName());
		return o;
	}
	

	private String getNameForPackageName(String pName) {
		if (pName == null)
			return null;
		// this package must be the default output
		if (pName.equals(getAndroidApplication().getPackageName())) {
			return null;
		}
		String ret = "";
		if (pName.contains(".")) {
			int lastDot = pName.lastIndexOf(".");
			ret = pName.substring(lastDot + 1).toUpperCase();
		}
		return ret;
	}

	/**
	 * Creates packages in the given src folder.
	 * 
	 * @param src
	 * @param allPackageNames
	 * @param monitor
	 * @return
	 * @throws CoreException
	 */
	private Map<String, IFolder> createPackages(IFolder src,
			List<String> allPackageNames, IProgressMonitor monitor)
			throws CoreException {
		Map<String, IFolder> packages = new HashMap<String, IFolder>();
		for (String packageName : allPackageNames) {
			if (packageName == null || packageName.isEmpty())
				continue;
			if (packageName.contains(".")) {
				String[] packageFragments = packageName.split("[//.]");
				IFolder fragmentContainer = src;
				for (String fragment : packageFragments) {
					fragmentContainer = createFolder(fragmentContainer,
							fragment, new SubProgressMonitor(monitor, 5));
				}
				if (fragmentContainer != null) {
					packages.put(packageName, fragmentContainer);
				}
			} else {
				IFolder folder = createFolder(src, packageName,
						new SubProgressMonitor(monitor, 5));
				if (folder != null) {
					packages.put(packageName, folder);
				}
			}
		}
		return packages;
	}

	/**
	 * Creates a folder in the given container.
	 * 
	 * @param container
	 * @param folderName
	 * @param monitor
	 * @throws CoreException
	 */
	private IFolder createFolder(IContainer container, String folderName,
			IProgressMonitor monitor) throws CoreException {
		if (folderName.length() > 0) {
			IFolder folder = container.getFolder(new Path(folderName));
			if (!folder.exists()) {
				folder.create(IResource.BACKGROUND_REFRESH, true, monitor);
			}
			return folder;
		}
		return null;
	}

	private void generateProject(IProgressMonitor monitor)
			throws AppGenerationException {
		monitor.subTask("Generating Android Project");
		SubProgressMonitor subMonitor = new SubProgressMonitor(monitor, 50);
		String projectName = getProjectName();
		try {
			IWorkspace ws = ResourcesPlugin.getWorkspace();
			project = ws.getRoot().getProject(projectName);
			projectDescription = ws.newProjectDescription(project.getName());
			if (project.exists()) {
				// TODO: regeneration
				project.delete(true, subMonitor);
			}
			Sdk sdk = getAndroidSdk();
			// FIXME android target only Google API 8, use the built in android project wizard
			IAndroidTarget target = getDefaultTarget(sdk);
			boolean legacy = target.getVersion().getApiLevel() < 4;
			// create the main android project
			project.create(projectDescription, subMonitor);
			if (subMonitor.isCanceled())
				throw new OperationCanceledException();
			// open the project
			project.open(IResource.BACKGROUND_REFRESH, subMonitor);
			// add java and android nature
			AndroidNature.setupProjectNatures(project, subMonitor);
			// create the folders in the project
			addDefaultDirectories(project, AndroidConstants.WS_ROOT,
					AndroidConstants2.DEFAULT_DIRECTORIES, subMonitor);
			String[] sourceFolders = new String[] { SdkConstants.FD_SOURCES,
					AndroidConstants2.GEN_SRC_DIRECTORY, IAppGenerator.FN_SRC_GEN };
			addDefaultDirectories(project, AndroidConstants.WS_ROOT,
					sourceFolders, subMonitor);
			// create resource folders
			if (legacy) {
				addDefaultDirectories(project, AndroidConstants2.RES_DIRECTORY,
						AndroidConstants2.RES_DIRECTORIES, subMonitor);
			} else {
				addDefaultDirectories(project, AndroidConstants2.RES_DIRECTORY,
						AndroidConstants2.RES_DENSITY_ENABLED_DIRECTORIES,
						monitor);
			}
			IJavaProject javaProject = JavaCore.create(project);
			setupSourceFolders(javaProject, sourceFolders, subMonitor);
			// add static resources like images to the res/* folders
			addResources(legacy, subMonitor);

			// set output location in the java project
			javaProject.setOutputLocation(
					project.getFolder(AndroidConstants2.BIN_DIRECTORY)
							.getFullPath(), subMonitor);

			sdk.initProject(project, target);

			// Fix the project to make sure all properties are as expected.
			// Necessary for existing projects and good for new ones to.
			ProjectHelper.fixProject(project);
		} catch (CoreException e) {
			throw new AppGenerationException(e.getMessage());
		} catch (IOException e) {
			throw new AppGenerationException(e.getMessage());
		} catch (StreamException e) {
			throw new AppGenerationException(e.getMessage());
		} finally {
			subMonitor.done();
		}
	}

	/**
	 * Returns the Android SDK.
	 * 
	 * @return
	 * @throws AppGenerationException
	 */
	private Sdk getAndroidSdk() throws AppGenerationException {
		Sdk sdk;
		String s = AdtPlugin.getOsSdkFolder();
		if (s.isEmpty()) {
			throw new AppGenerationException(
					"SDKfolder is null, enter the SDK folder name!");
		}
		if (Sdk.getCurrent() == null) {
			sdk = Sdk.loadSdk(s);
		} else {
			sdk = Sdk.getCurrent();
		}
		return sdk;
	}

	/**
	 * Returns the default target: Google API 8.
	 * 
	 * @param sdk
	 * @return
	 * @throws AppGenerationException
	 */
	private IAndroidTarget getDefaultTarget(Sdk sdk)
			throws AppGenerationException {
		IAndroidTarget target = null;
		for (IAndroidTarget t : sdk.getTargets()) {
			if (t.getVersion().getApiLevel() == 8
					&& t.getVendor().contains("Google")) {
				target = t;
				break;
			}
		}
		return target;
	}

	private void addResources(boolean legacy, IProgressMonitor monitor)
			throws CoreException {
		addIcon(getProject(), legacy, monitor);
	}

	/**
	 * Adds default application icon to the project.
	 * 
	 * @param project
	 *            The Java Project to update.
	 * @param legacy
	 *            whether we're running in legacy mode (no density support)
	 * @param monitor
	 *            An existing monitor.
	 * @throws CoreException
	 *             if the method fails to update the project.
	 */
	private void addIcon(IProject project, boolean legacy,
			IProgressMonitor monitor) throws CoreException {
		if (legacy) { // density support
			// do medium density icon only, in the default drawable folder.
			IFile file = project.getFile(AndroidConstants2.RES_DIRECTORY
					+ AndroidConstants.WS_SEP
					+ AndroidConstants2.DRAWABLE_DIRECTORY
					+ AndroidConstants.WS_SEP + AndroidConstants2.PROJECT_ICON);
			if (!file.exists()) {
				addFile(file,
						AdtPlugin
								.readEmbeddedFile(AndroidConstants2.TEMPLATES_DIRECTORY
										+ AndroidConstants2.ICON_MDPI), monitor);
			}
		} else {
			// do all 3 icons.
			IFile file;

			// high density
			file = project.getFile(AndroidConstants2.RES_DIRECTORY
					+ AndroidConstants.WS_SEP
					+ AndroidConstants2.DRAWABLE_HDPI_DIRECTORY
					+ AndroidConstants.WS_SEP + AndroidConstants2.PROJECT_ICON);
			if (!file.exists()) {
				addFile(file,
						AdtPlugin
								.readEmbeddedFile(AndroidConstants2.TEMPLATES_DIRECTORY
										+ AndroidConstants2.ICON_HDPI), monitor);
			}

			// medium density
			file = project.getFile(AndroidConstants2.RES_DIRECTORY
					+ AndroidConstants.WS_SEP
					+ AndroidConstants2.DRAWABLE_MDPI_DIRECTORY
					+ AndroidConstants.WS_SEP + AndroidConstants2.PROJECT_ICON);
			if (!file.exists()) {
				addFile(file,
						AdtPlugin
								.readEmbeddedFile(AndroidConstants2.TEMPLATES_DIRECTORY
										+ AndroidConstants2.ICON_MDPI), monitor);
			}

			// low density
			file = project.getFile(AndroidConstants2.RES_DIRECTORY
					+ AndroidConstants.WS_SEP
					+ AndroidConstants2.DRAWABLE_LDPI_DIRECTORY
					+ AndroidConstants.WS_SEP + AndroidConstants2.PROJECT_ICON);
			if (!file.exists()) {
				addFile(file,
						AdtPlugin
								.readEmbeddedFile(AndroidConstants2.TEMPLATES_DIRECTORY
										+ AndroidConstants2.ICON_LDPI), monitor);
			}
		}
	}

	private void addFile(IFile dest, byte[] source, IProgressMonitor monitor)
			throws CoreException {
		if (source != null) {
			// Save in the project
			InputStream stream = new ByteArrayInputStream(source);
			dest.create(stream, false /* force */, new SubProgressMonitor(
					monitor, 10));
		}
	}

	private void setupSourceFolders(IJavaProject javaProject,
			String[] sourceFolders, IProgressMonitor monitor)
			throws JavaModelException {
		IProject project = javaProject.getProject();

		// get the list of entries.
		IClasspathEntry[] entries = javaProject.getRawClasspath();

		// remove the project as a source folder (This is the default)
		entries = removeSourceClasspath(entries, project);

		// add the source folders.
		for (String sourceFolder : sourceFolders) {
			IFolder srcFolder = project.getFolder(sourceFolder);

			// remove it first in case.
			entries = removeSourceClasspath(entries, srcFolder);
			entries = ProjectHelper.addEntryToClasspath(entries,
					JavaCore.newSourceEntry(srcFolder.getFullPath()));
		}
		javaProject.setRawClasspath(entries,
				new SubProgressMonitor(monitor, 10));
	}

	private IClasspathEntry[] removeSourceClasspath(IClasspathEntry[] entries,
			IContainer folder) {
		if (folder == null) {
			return entries;
		}
		IClasspathEntry source = JavaCore.newSourceEntry(folder.getFullPath());
		int n = entries.length;
		for (int i = 0; i < n; i++) {
			if (entries[i].equals(source)) {
				IClasspathEntry[] newEntries = new IClasspathEntry[n - 1];
				if (i > 0) {
					System.arraycopy(entries, 0, newEntries, 0, i);
				}
				if (i < n - 1) {
					System.arraycopy(entries, i + 1, newEntries, i, n - i - 1);
				}
				n--;
				entries = newEntries;
			}
		}
		return entries;
	}

	/**
	 * Adds default directories to the project.
	 * 
	 * @param project
	 *            The Java project to update.
	 * @param parentFolder
	 *            The path of the parent folder. Must end with a separator.
	 * @param folders
	 *            Folders to be added.
	 * @param monitor
	 *            An existing monitor.
	 * @throws CoreException
	 *             if the method fails to create the directories in the project.
	 */
	private void addDefaultDirectories(IProject project, String parentFolder,
			String[] folders, IProgressMonitor monitor) throws CoreException {
		for (String name : folders) {
			if (name.length() > 0) {
				IFolder folder = project.getFolder(parentFolder + name);
				if (!folder.exists()) {
					folder.create(true /* force */, true /* local */,
							new SubProgressMonitor(monitor, 10));
				}
			}
		}
	}

	@Override
	public IFile getSource() {
		return source;
	}

	@Override
	public AndroidApplication getAndroidApplication() {
		return application;
	}

	@Override
	public String getProjectName() {
		return getAndroidApplication().getProjectName();
	}

	@Override
	public IProject getProject() {
		return project;
	}

	@Override
	public String getLocationRoot() {
		return getProject().getLocation().toOSString() + "/";
	}

	@Override
	public IProjectDescription getProjectDescription() {
		return projectDescription;
	}

	@Override
	public void setProject(IProject project) {
		this.project = project;
	}

	@Override
	public void unload() {
		getAndroidApplication().eResource().unload();
	}

	@Override
	public List<String> getAllPackageNames() {
		if (allPackageNames == null) {
			allPackageNames = new ArrayList<String>();
			allPackageNames.add(getAndroidApplication().getPackageName());
			// data package for entity generation
			allPackageNames.add(getAndroidApplication().getPackageName() + ".data");
		}
		return allPackageNames;
	}

	@Override
	public Output getOutput() {
		return output;
	}

	@Override
	public IProject getModelProject() {
		return modelProject;
	}

}
